package org.stars.service;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.stars.contract.StarsContract;
import org.stars.message.ErrorMessage;
import org.stars.message.Message;
import org.stars.service.pub.EthPublic;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.*;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.admin.methods.response.BooleanResponse;
import org.web3j.protocol.admin.methods.response.PersonalUnlockAccount;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.RemoteCall;
import org.web3j.protocol.core.RemoteFunctionCall;
import org.web3j.protocol.core.Request;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.protocol.geth.Geth;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.Transfer;
import org.web3j.tx.gas.StaticGasProvider;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;

/**
 * @author 沧月
 * @date 2020/12/30
 * @desc 描述
 */
@Service
@Slf4j
public class EthService {

    @Autowired
    private Web3j web3j;

    @Autowired
    private Admin admin;

    @Autowired
    private Geth geth;

    @Value("${eth.rinked.password}")
    private String password;

    @Value("${eth.rinked.keystore-path}")
    private String keystore;

    @Value("${eth.unlock-time}")
    private String unlockTime;

    public Message<String> findClientVersion() {
        Web3ClientVersion web3ClientVersion = null;
        try {
            web3ClientVersion = web3j.web3ClientVersion().send();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        String clientVersion = web3ClientVersion.getWeb3ClientVersion();
        return new Message<>().setData(clientVersion);
    }

    public Message<JSONObject> newAccount() {
        String walletFileName = null;
        try {
            walletFileName = WalletUtils.generateNewWalletFile(password, new File(keystore), false);
        } catch (CipherException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        }

        Credentials credentials = EthPublic.credentials(password, keystore + walletFileName);
        // 获取钱包地址，私钥，公钥
        String address = credentials.getAddress();
        String publicKey = credentials.getEcKeyPair().getPublicKey().toString(16);
        String privateKey = credentials.getEcKeyPair().getPrivateKey().toString(16);
        return new Message<>().setData(new JSONObject()
                .fluentPut("address", address)
                .fluentPut("publicKey", publicKey)
                .fluentPut("credentials", walletFileName)
                .fluentPut("privateKey", privateKey)
        );
    }


    public Message<String> findEthBalance(String address) {
        EthGetBalance ethGetBlance = null;
        try {
            ethGetBlance = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        BigInteger balance1 = ethGetBlance.getBalance();
        BigDecimal bigDecimalBalance = new BigDecimal(balance1);
        String balance = Convert.fromWei(bigDecimalBalance.toString(), Convert.Unit.ETHER).toPlainString();
        return new Message<>().setData(balance);
    }


    public BigInteger gasPrice() {
        EthGasPrice ethGasPrice = null;
        try {
            ethGasPrice = web3j.ethGasPrice().sendAsync().get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        return ethGasPrice.getGasPrice();
    }

    /**
     * 发起一笔交易
     * 使用以太钱包文件发送以太币给其他人，不能设置nonce：（推荐）
     *
     * @param privateKey
     */
    public String transfer(String toAddress, BigDecimal value, String privateKey) throws Exception {
        //转账者私钥
        Credentials credentials = Credentials.create(privateKey);
        TransactionReceipt transactionReceipt = Transfer.sendFunds(
                web3j, credentials, toAddress,
                value, Convert.Unit.ETHER).sendAsync().get();
        String transactionHash = transactionReceipt.getTransactionHash();
        return transactionHash;
    }


    /**
     * 解锁账户，发送交易前需要对账户进行解锁
     *
     * @param address  地址
     * @param password 密码
     * @param duration 解锁有效时间，单位秒
     * @return
     * @throws IOException
     */
    public Boolean unlockAccount(String address, String password, BigInteger duration) {
        Request<?, PersonalUnlockAccount> request = admin.personalUnlockAccount(address, password, duration);
        PersonalUnlockAccount account = null;
        try {
            account = request.send();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        return account.accountUnlocked();
    }

    /**
     * 账户锁定，使用完成之后需要锁定
     *
     * @param address
     * @return
     * @throws IOException
     */
    public Boolean lockAccount(String address) {
        Request<?, BooleanResponse> request = geth.personalLockAccount(address);
        BooleanResponse response = null;
        try {
            response = request.send();
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        return response.success();
    }


    private BigInteger getNonce(Web3j web3j, String fromAddress) {
        try {
            EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount(
                    fromAddress, DefaultBlockParameterName.LATEST).sendAsync().get();
            return ethGetTransactionCount.getTransactionCount();
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
        } catch (ExecutionException e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }


    private Message<String> ethSendRawTransaction(Web3j web3j, String hexValue) {
        EthSendTransaction ethSendTransaction = null;
        String transactionHash = null;
        try {
            ethSendTransaction = web3j.ethSendRawTransaction(hexValue).send();
        } catch (IOException e) {
            e.printStackTrace();
            return new Message<>(e.getMessage());
        }
        if (ethSendTransaction.hasError()) {
            log.info("transfer error:", ethSendTransaction.getError().getMessage());
            return new Message<>(ethSendTransaction.getError().getMessage());
        } else {
            transactionHash = ethSendTransaction.getTransactionHash();
            log.info("Transfer transactionHash:" + transactionHash);
        }
        return new Message<>().setData(transactionHash);
    }


    /**
     * ETH转账
     *
     * @param fromAddress
     * @param toAddress
     * @return
     */
    public Message<String> ethTransaction(String fromAddress, String toAddress, String value, String gasLimit, String privateKye) {
        Credentials credentials = EthPublic.credentials(password, privateKye);
        unlockAccount(fromAddress, password, new BigInteger(unlockTime));
        BigInteger nonce = this.getNonce(web3j, fromAddress);
        String bigDecimal = Convert.toWei(value, Convert.Unit.ETHER).toString();
        int i = bigDecimal.indexOf(".");
        if (i < 0) {
            value = bigDecimal;
        } else {
            value = bigDecimal.substring(0, i);
        }
        RawTransaction rawTransaction = RawTransaction.createEtherTransaction(
                nonce, this.gasPrice(),
                Convert.toWei(gasLimit, Convert.Unit.WEI).toBigInteger(), toAddress, new BigInteger(value));
        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        String hexValue = Numeric.toHexString(signedMessage);
        Message<String> stringMessage = this.ethSendRawTransaction(web3j, hexValue);
//        lockAccount(fromAddress); //未开放这个方法吧
        return stringMessage;
    }


    /**
     * ETH代币转账
     *
     * @param fromAddress
     * @param toAddress
     * @return
     */
    public Message<String> ethTokenTransaction(String fromAddress, String toAddress, String contractAddress, String value,
                                               String gasLimit, String privateKye) {
        Credentials credentials = EthPublic.credentials(password, privateKye);
        BigInteger nonce = this.getNonce(web3j, fromAddress);

        Function function = new Function(
                "transfer",
                Arrays.asList(new Address(toAddress), new Uint256(new BigInteger(value))),
                Arrays.asList(new TypeReference<Type>() {
                }));

        String encodedFunction = FunctionEncoder.encode(function);

        RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, this.gasPrice(),
                Convert.toWei(gasLimit, Convert.Unit.WEI).toBigInteger()
                , contractAddress, encodedFunction);

        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        String hexValue = Numeric.toHexString(signedMessage);
        Message<String> stringMessage = this.ethSendRawTransaction(web3j, hexValue);
        return stringMessage;
    }


    //合同部署
    public Message<String> deployContract(String privateKye) {
        Credentials credentials = EthPublic.credentials(password, privateKye);
        BigInteger bigInteger = this.gasPrice();
        RemoteCall<StarsContract> deploy = StarsContract.deploy(web3j, credentials, new StaticGasProvider(bigInteger, new BigInteger("3000000")));
        try {
            String contractAddress = deploy.send().getContractAddress();
            return new Message<>().setData(contractAddress);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return new Message<>("智能合同部署失败");
    }


    //合同交易
    public Message<String> contractTransaction(String contractAddress, String toAddress, String value, String privateKye, String gasLimit) {
        StarsContract load = this.getContract(contractAddress, privateKye, gasLimit);
        TransactionReceipt send = null;
        try {
            RemoteFunctionCall<TransactionReceipt> transfer = load.transfer(toAddress, new BigInteger(value));
            send = transfer.sendAsync().get();
            if (send.isStatusOK()) {
                return new Message<>().setData(send.getTransactionHash());
            }
        } catch (Exception e) {
            e.printStackTrace();
            return new ErrorMessage<>(e);
        }
        return new ErrorMessage<>("交易失败");
    }


    //设置代币余额
    public Message<String> issue(String contractAddress, String privateKye, String _recipient, String value, String gasLimit) {
        StarsContract contract = this.getContract(contractAddress, privateKye, gasLimit);
        try {
            TransactionReceipt transactionReceipt = contract.issue(_recipient, new BigInteger(value)).send();
            boolean statusOK = transactionReceipt.isStatusOK();
            if (statusOK) {
                return new Message<>().setData("操作成功");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ErrorMessage<>("操作失败");
    }

    //获取代币余额
    public Message<Integer> eth20Balance(String address, String contractAddress, String privateKye, String gasLimit) {
        Credentials credentials = EthPublic.credentials(password, privateKye);
        StarsContract starsContract = StarsContract.load(contractAddress, web3j, credentials,
                new StaticGasProvider(this.gasPrice(), new BigInteger(gasLimit)));
        RemoteFunctionCall<BigInteger> remoteFunctionCall = starsContract.balanceOf(address);
        try {
            int value = remoteFunctionCall.send().intValue();
            return new Message<>().setData(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    private StarsContract getContract(String contractAddress, String privateKye, String gasLimit) {
        Credentials credentials = EthPublic.credentials(password, privateKye);
        return StarsContract.load(contractAddress, web3j, credentials, new StaticGasProvider(this.gasPrice(), new BigInteger(gasLimit)));
    }

    public String test() {
        try {
            Credentials credentials = WalletUtils.loadCredentials(password, keystore + "UTC--2021-01-04T06-22-34.37042000Z--56517cfe55e2dfa4d8acf190231d9ff097d5a61e.json");

            return credentials.getEcKeyPair().getPrivateKey().toString(16);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (CipherException e) {
            e.printStackTrace();
        }
        return null;
    }

}
